﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using EnvDTE80;
using Microsoft.VisualStudio.Shell.Interop;
using Process = EnvDTE.Process;

namespace PeterKuhn.SilverlightDebugHelper
{
    /// <summary>
    /// A helper class that tries to attach to the browser processes that host the Silverlight runtime.
    /// </summary>
    public class ProcessHelper
    {
        private readonly Settings _settings;
        private DTE2 _dte2;
        private IVsSolution _solution;

        private volatile int _processCheckCount;
        private System.Threading.Timer _timer;

        private readonly List<int> _successfulIds;

        /// <summary>
        /// Initializes a new instance of the <see cref="ProcessHelper"/> class.
        /// </summary>
        /// <param name="settings">The settings to use.</param>
        public ProcessHelper(Settings settings)
        {
            _settings = settings;
            _successfulIds = new List<int>(_settings.SupportedBrowsers.Count());
        }

        private void Log(LogLevel logLevel, string message, params string[] args)
        {
#if !DEBUG
            if (logLevel < LogLevel.Info)
            {
                return;
            }
#endif

            if (args != null && args.Length > 0)
            {
                message = string.Format(message, args);
            }

            if (_dte2 != null)
            {
                _dte2.OutputFormattedDebugText(message);
            }
            else
            {
                Debug.WriteLine(message);
            }
        }

        /// <summary>
        /// Initializes the helper with the specified application object.
        /// </summary>
        /// <param name="serviceProvider">The provider that grants access to various services.</param>
        public void Initialize(IServiceProvider serviceProvider)
        {
            _dte2 = serviceProvider.GetService(typeof(SDTE)) as DTE2;
            _solution = serviceProvider.GetService(typeof(SVsSolution)) as IVsSolution;
        }

        /// <summary>
        /// Tries to attach to the browser processes.
        /// </summary>
        public void TryAttachToProcesses()
        {
            var projectNames = RetrieveProjectNames();
            var silverlightProjects = DetermineSilverlightProjects(projectNames);

            if (silverlightProjects.Count == 0)
            {
                // no Silverlight projects
                Log(LogLevel.Info, "No Silverlight project found. Taking no further action.");
                return;
            }

            // ought for a check for the project assemblies to avoid 
            // attaching to random browser processes which host Silverlight
            // => not yet working (how do you get the hosted Silverlight assemblies in an unmanaged process?)
            //// get the assembly names of the Silverlight projects
            //List<string> silverlightAssemblyNames = new List<string>();
            //foreach (var project in projects)
            //{
            //    string assemblyName = project.GetProjectAssemblyName();

            //    if (string.IsNullOrEmpty(assemblyName))
            //    {
            //        Log("Could not determine assembly name for project {0}. Skipping...", project.Name);
            //        continue;
            //    }

            //    Log("Adding assembly {0} to the search.", assemblyName);
            //    silverlightAssemblyNames.Add(assemblyName);
            //}

            //if (silverlightAssemblyNames.Count == 0)
            //{
            //    Log("No Silverlight assembly names to process. Taking no further action.");
            //    return;
            //}

            // enable the timer, that does the actual attaching
            _processCheckCount = 0;
            _successfulIds.Clear();
            EnableTimer();
        }

        private IEnumerable<string> RetrieveProjectNames()
        {
            // get the size
            uint numberOfProjects;
            _solution.GetProjectFilesInSolution((uint)__VSGETPROJFILESFLAGS.GPFF_SKIPUNLOADEDPROJECTS, 0, null, out numberOfProjects);

            // now get the names
            var projectNames = new string[numberOfProjects];
            _solution.GetProjectFilesInSolution((uint)__VSGETPROJFILESFLAGS.GPFF_SKIPUNLOADEDPROJECTS, numberOfProjects, projectNames, out numberOfProjects);

            return projectNames;
        }

        private IList<IVsHierarchy> DetermineSilverlightProjects(IEnumerable<string> projectNames)
        {
            var result = new List<IVsHierarchy>();

            foreach (var projectName in projectNames)
            {
                IVsHierarchy hierarchy;
                var returnCode = _solution.GetProjectOfUniqueName(projectName, out hierarchy);
                if (returnCode != 0)
                {
                    continue;
                }

                var project = hierarchy as IVsAggregatableProject;
                if (project == null)
                {
                    continue;
                }

                string projectTypeGuids;
                returnCode = project.GetAggregateProjectTypeGuids(out projectTypeGuids);
                if (returnCode != 0 || string.IsNullOrEmpty(projectTypeGuids))
                {
                    continue;
                }

                var projectTypeGuidParts = projectTypeGuids.Split(';');
                foreach (var projectTypeGuidPart in projectTypeGuidParts)
                {
                    var tempGuid = new Guid(projectTypeGuidPart);
                    if (tempGuid.Equals(Constants.SilverlightProjectTypeGuid))
                    {
                        result.Add(hierarchy);
                    }
                }
            }

            return result;
        }

        private void AttachToProcessTimerTick(object state)
        {
            // for safety reasons, we disable the timer here            
            DisableTimer();

            // we only do our checks for a certain period of time
            _processCheckCount++;

            // do this for every configured browser
            var configurations = _settings.SelectedBrowsers.ToArray();

            foreach (var configuration in configurations)
            {
                // did we already successfully process this configuration?
                if (_successfulIds.Contains(configuration.ConfigurationId))
                {
                    continue;
                }

                Log(LogLevel.Debug, "Processing selected browser {0}...", configuration.DisplayName);

                // some flags
                bool foundProcess = false;
                bool attachedToProcess = false;

                // check every process name
                foreach (var processName in configuration.ProcessNames)
                {
                    // ok, not yet attached => check for the process in the local processes
                    foreach (Process localProcess in _dte2.Debugger.LocalProcesses)
                    {
                        // check name
                        string lowerLocalProcessName = localProcess.Name.ToLower();
                        if (lowerLocalProcessName.Contains(processName))
                        {
                            foundProcess = true;

                            // ok, we're not attached => check if the SL runtime is loaded
                            if (!localProcess.HasLoadedModule(Constants.SilverlightCoreModule))
                            {
                                // might not be ready yet, or another process of that browser
                                continue;
                            }

                            // now let's see if we're attached to the process already
                            if (_dte2.Debugger.IsDebuggingProcess(localProcess.ProcessID))
                            {
                                // change flag
                                attachedToProcess = true;

                                // we're already attached to the process
                                //Log("Already attached to process {0} ({1}). Taking no further action.", processName, localProcess.ProcessID.ToString());
                                continue;
                            }

                            // wait for input idle => disabled, causes problems sometimes
                            //localProcess.WaitForInputIdle();

                            // ready, attach!
                            try
                            {
                                Process2 localProcess2 = localProcess as Process2;
                                if (localProcess2 != null)
                                {
                                    localProcess2.Attach2(Constants.SilverlightDebugEngineGuidString);
                                }
                                else
                                {
                                    localProcess.Attach();
                                }

                                attachedToProcess = true;
                                Log(LogLevel.Debug, "Successfully attached to process {0}.", processName);
                            }
                            catch (Exception ex)
                            {
                                // explicitly set the flag back to false
                                // so we can try again next time (it might be set to true when attaching to multiple processes)
                                attachedToProcess = false;

                                // just let someone know about this
                                Log(LogLevel.Exception, "*** Exception when attaching to the process {0}: {1}", processName, ex.ToString());
                            }
                        }
                    }
                }

                // check if we were successful
                if (attachedToProcess)
                {
                    _successfulIds.Add(configuration.ConfigurationId);
                    Log(LogLevel.Info, "Successfully attached to browser {0}.", configuration.DisplayName);
                }
                else
                {
                    if (foundProcess)
                    {
                        // if we could not attach but found the process, we should try a bit harder...
                        Log(LogLevel.Debug, "Found the required process, but it has not loaded the Silverlight runtime (yet?). Waiting a bit more...");
                    }
                    else
                    {
                        // if we could not attach but found the process, we should try a bit harder...
                        Log(LogLevel.Debug, "Did not find the required process. Waiting a bit more...");
                    }
                }
            }

            // check if there are unsuccessful browser configurations
            var remainingConfigurations = (from o in configurations
                                           where (!_successfulIds.Contains(o.ConfigurationId))
                                           select o).ToList();

            if (remainingConfigurations.Count > 0)
            {
                if (_processCheckCount < Constants.MaxNumberOfAttempts)
                {
                    // re-enable the timer
                    EnableTimer();
                }
                else
                {
                    var sb = new StringBuilder();
                    for (int i = 0; i < remainingConfigurations.Count; i++)
                    {
                        if (i > 0)
                        {
                            sb.Append(", ");
                        }
                        sb.Append(remainingConfigurations[i].DisplayName);
                    }

                    Log(LogLevel.Info, "Could not attach to the following browsers: {0}", sb.ToString());
                }
            }
            else
            {
                Log(LogLevel.Debug, "Successfully attached to all configured browsers.");
            }
        }

        private void EnableTimer()
        {
            if (_timer == null)
            {
                _timer = new System.Threading.Timer(
                        AttachToProcessTimerTick,
                        null,
                        1000,
                        1000
                    );
            }
            else
            {
                _timer.Change(1000, 1000);
            }
        }

        private void DisableTimer()
        {
            _timer.Change(System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
        }
    }
}
